from nest.meta import Meta
from nest.path import Path
from nest.path import ParsedPath
from nest.path.parsed import PathKey
from nest.tree.exception import (PathError, MapError, TraverseError)


class AbstractTree(Meta):

    @property
    def default_value(self):
        return None

    def has_path(self, path):
        try:
            self.get(path)
        except PathError:
            return False
        return True

    def contains(self, subtree):
        '''
        Test if this tree contains a given subtree.
        '''
        raise Exception('Not implemented.')

    def get(self, path):
        '''
        Obtain a subtree at a given path.
        '''
        tree = self
        path = Path.resolve_path(tree, path)
        # Iterate over path to get a subtree.
        for key in path:
            subtree = tree.get(key)
            if subtree is None or subtree == tree:
                raise TraverseError()
            # Go deeper into recursion.
            tree = subtree
        return tree

    def set(self, path, value, make_parents=True):
        '''
        @type  make_parents: boolean
        @param make_parents: Make parent directories as needed.

        @return: AbstractTree
        '''
        path = Path.resolve_path(self, path)
        if not self.has_path(path):
            if not make_parents:
                raise PathError()
            # Make missing path tail.
            tree = self
            for key in path:
                try:
                    subtree = tree.get(key)
                except PathError:
                    # Will construct a single key.
                    subtree = tree.set(key, self.default_value).get(key)
                if subtree is None or subtree == tree:
                    raise Exception('Failed to construct a subtree')
                tree = subtree
        tree = self
        parent_path = Path.get_parent(tree, path)
        key = Path.get_tail(tree, path)
        tree.get(parent_path).set(key, value)
        return self

    def add(self, path, value):
        return self.set(path, value, make_parents=True)

    def cut(self, path):
        return self

    def clear(self, path):
        return self

    def map(self, callback=None):
        result = self.make()
        for path in self.paths():
            node = self.get(path)
            if callback is not None:
                node = callback(path, node)
            if node is not None:
                result.set(path, node.value)
        return result

    def paths(self, join_paths=False):
        '''
        Discover all paths in top->bottom, left->right (if possible) order.
        '''
        paths = list()
        for key in self.keys():
            paths.append(key)
            subtree = self.get(key)
            for path in subtree.paths():
                if isinstance(path, basestring):
                    path = ParsedPath([PathKey(path)])
                paths.append(PathKey(key) + path)
        if join_paths:
            return self._join_paths(paths)
        return paths

    def keys(self):
        raise PathError()

    def _join_paths(self, paths):
        join = Path.get_join(self)
        joined_paths = list()
        for path in paths:
            if not isinstance(path, basestring):
                path = join(path)
            joined_paths.append(path)
        return joined_paths
